import defaultValue from "../Core/defaultValue.js";
import defined from "../Core/defined.js";
import WebGLConstants from "../Core/WebGLConstants.js";
import webGLConstantToGlslType from "../Core/webGLConstantToGlslType.js";
import addToArray from "./GltfPipeline/addToArray.js";
import ForEach from "./GltfPipeline/ForEach.js";
import usesExtension from "./GltfPipeline/usesExtension.js";
import ModelUtility from "./ModelUtility.js";

/**
 * @private
 */
function processModelMaterialsCommon(gltf, options) {
  options = defaultValue(options, defaultValue.EMPTY_OBJECT);

  if (!defined(gltf)) {
    return;
  }

  if (!usesExtension(gltf, "KHR_materials_common")) {
    return;
  }

  if (!usesExtension(gltf, "KHR_techniques_webgl")) {
    if (!defined(gltf.extensions)) {
      gltf.extensions = {};
    }

    gltf.extensions.KHR_techniques_webgl = {
      programs: [],
      shaders: [],
      techniques: [],
    };
    gltf.extensionsUsed.push("KHR_techniques_webgl");
    gltf.extensionsRequired.push("KHR_techniques_webgl");
  }

  const techniquesWebgl = gltf.extensions.KHR_techniques_webgl;

  lightDefaults(gltf);

  const lightParameters = generateLightParameters(gltf);

  const primitiveByMaterial = ModelUtility.splitIncompatibleMaterials(gltf);

  const techniques = {};
  let generatedTechniques = false;
  ForEach.material(gltf, function (material, materialIndex) {
    if (
      defined(material.extensions) &&
      defined(material.extensions.KHR_materials_common)
    ) {
      const khrMaterialsCommon = material.extensions.KHR_materials_common;
      const primitiveInfo = primitiveByMaterial[materialIndex];

      const techniqueKey = getTechniqueKey(khrMaterialsCommon, primitiveInfo);
      let technique = techniques[techniqueKey];

      if (!defined(technique)) {
        technique = generateTechnique(
          gltf,
          techniquesWebgl,
          primitiveInfo,
          khrMaterialsCommon,
          lightParameters,
          options.addBatchIdToGeneratedShaders
        );
        techniques[techniqueKey] = technique;
        generatedTechniques = true;
      }

      const materialValues = {};
      const values = khrMaterialsCommon.values;
      let uniformName;
      for (const valueName in values) {
        if (
          values.hasOwnProperty(valueName) &&
          valueName !== "transparent" &&
          valueName !== "doubleSided"
        ) {
          uniformName = `u_${valueName.toLowerCase()}`;
          materialValues[uniformName] = values[valueName];
        }
      }

      material.extensions.KHR_techniques_webgl = {
        technique: technique,
        values: materialValues,
      };

      material.alphaMode = "OPAQUE";
      if (khrMaterialsCommon.transparent) {
        material.alphaMode = "BLEND";
      }

      if (khrMaterialsCommon.doubleSided) {
        material.doubleSided = true;
      }
    }
  });

  if (!generatedTechniques) {
    return gltf;
  }

  // If any primitives have semantics that aren't declared in the generated
  // shaders, we want to preserve them.
  ModelUtility.ensureSemanticExistence(gltf);

  return gltf;
}

function generateLightParameters(gltf) {
  const result = {};

  let lights;
  if (
    defined(gltf.extensions) &&
    defined(gltf.extensions.KHR_materials_common)
  ) {
    lights = gltf.extensions.KHR_materials_common.lights;
  }

  if (defined(lights)) {
    // Figure out which node references the light
    const nodes = gltf.nodes;
    for (const nodeName in nodes) {
      if (nodes.hasOwnProperty(nodeName)) {
        const node = nodes[nodeName];
        if (
          defined(node.extensions) &&
          defined(node.extensions.KHR_materials_common)
        ) {
          const nodeLightId = node.extensions.KHR_materials_common.light;
          if (defined(nodeLightId) && defined(lights[nodeLightId])) {
            lights[nodeLightId].node = nodeName;
          }
          delete node.extensions.KHR_materials_common;
        }
      }
    }

    // Add light parameters to result
    let lightCount = 0;
    for (const lightName in lights) {
      if (lights.hasOwnProperty(lightName)) {
        const light = lights[lightName];
        const lightType = light.type;
        if (lightType !== "ambient" && !defined(light.node)) {
          delete lights[lightName];
          continue;
        }
        const lightBaseName = `light${lightCount.toString()}`;
        light.baseName = lightBaseName;
        let ambient;
        let directional;
        let point;
        let spot;
        switch (lightType) {
          case "ambient":
            ambient = light.ambient;
            result[`${lightBaseName}Color`] = {
              type: WebGLConstants.FLOAT_VEC3,
              value: ambient.color,
            };
            break;
          case "directional":
            directional = light.directional;
            result[`${lightBaseName}Color`] = {
              type: WebGLConstants.FLOAT_VEC3,
              value: directional.color,
            };
            if (defined(light.node)) {
              result[`${lightBaseName}Transform`] = {
                node: light.node,
                semantic: "MODELVIEW",
                type: WebGLConstants.FLOAT_MAT4,
              };
            }
            break;
          case "point":
            point = light.point;
            result[`${lightBaseName}Color`] = {
              type: WebGLConstants.FLOAT_VEC3,
              value: point.color,
            };
            if (defined(light.node)) {
              result[`${lightBaseName}Transform`] = {
                node: light.node,
                semantic: "MODELVIEW",
                type: WebGLConstants.FLOAT_MAT4,
              };
            }
            result[`${lightBaseName}Attenuation`] = {
              type: WebGLConstants.FLOAT_VEC3,
              value: [
                point.constantAttenuation,
                point.linearAttenuation,
                point.quadraticAttenuation,
              ],
            };
            break;
          case "spot":
            spot = light.spot;
            result[`${lightBaseName}Color`] = {
              type: WebGLConstants.FLOAT_VEC3,
              value: spot.color,
            };
            if (defined(light.node)) {
              result[`${lightBaseName}Transform`] = {
                node: light.node,
                semantic: "MODELVIEW",
                type: WebGLConstants.FLOAT_MAT4,
              };
              result[`${lightBaseName}InverseTransform`] = {
                node: light.node,
                semantic: "MODELVIEWINVERSE",
                type: WebGLConstants.FLOAT_MAT4,
                useInFragment: true,
              };
            }
            result[`${lightBaseName}Attenuation`] = {
              type: WebGLConstants.FLOAT_VEC3,
              value: [
                spot.constantAttenuation,
                spot.linearAttenuation,
                spot.quadraticAttenuation,
              ],
            };

            result[`${lightBaseName}FallOff`] = {
              type: WebGLConstants.FLOAT_VEC2,
              value: [spot.fallOffAngle, spot.fallOffExponent],
            };
            break;
        }
        ++lightCount;
      }
    }
  }

  return result;
}

function generateTechnique(
  gltf,
  techniquesWebgl,
  primitiveInfo,
  khrMaterialsCommon,
  lightParameters,
  addBatchIdToGeneratedShaders
) {
  if (!defined(khrMaterialsCommon)) {
    khrMaterialsCommon = {};
  }

  addBatchIdToGeneratedShaders = defaultValue(
    addBatchIdToGeneratedShaders,
    false
  );

  const techniques = techniquesWebgl.techniques;
  const shaders = techniquesWebgl.shaders;
  const programs = techniquesWebgl.programs;
  const lightingModel = khrMaterialsCommon.technique.toUpperCase();
  let lights;
  if (
    defined(gltf.extensions) &&
    defined(gltf.extensions.KHR_materials_common)
  ) {
    lights = gltf.extensions.KHR_materials_common.lights;
  }

  const parameterValues = khrMaterialsCommon.values;
  const jointCount = defaultValue(khrMaterialsCommon.jointCount, 0);

  let skinningInfo;
  let hasSkinning = false;
  let hasVertexColors = false;

  if (defined(primitiveInfo)) {
    skinningInfo = primitiveInfo.skinning;
    hasSkinning = skinningInfo.skinned;
    hasVertexColors = primitiveInfo.hasVertexColors;
  }

  let vertexShader = "precision highp float;\n";
  let fragmentShader = "precision highp float;\n";

  const hasNormals = lightingModel !== "CONSTANT";

  // Add techniques
  const techniqueUniforms = {
    u_modelViewMatrix: {
      semantic: usesExtension(gltf, "CESIUM_RTC")
        ? "CESIUM_RTC_MODELVIEW"
        : "MODELVIEW",
      type: WebGLConstants.FLOAT_MAT4,
    },
    u_projectionMatrix: {
      semantic: "PROJECTION",
      type: WebGLConstants.FLOAT_MAT4,
    },
  };

  if (hasNormals) {
    techniqueUniforms.u_normalMatrix = {
      semantic: "MODELVIEWINVERSETRANSPOSE",
      type: WebGLConstants.FLOAT_MAT3,
    };
  }

  if (hasSkinning) {
    techniqueUniforms.u_jointMatrix = {
      count: jointCount,
      semantic: "JOINTMATRIX",
      type: WebGLConstants.FLOAT_MAT4,
    };
  }

  // Add material values
  let uniformName;
  let hasTexCoords = false;
  for (const name in parameterValues) {
    //generate shader parameters for KHR_materials_common attributes
    //(including a check, because some boolean flags should not be used as shader parameters)
    if (
      parameterValues.hasOwnProperty(name) &&
      name !== "transparent" &&
      name !== "doubleSided"
    ) {
      const uniformType = getKHRMaterialsCommonValueType(
        name,
        parameterValues[name]
      );
      uniformName = `u_${name.toLowerCase()}`;
      if (!hasTexCoords && uniformType === WebGLConstants.SAMPLER_2D) {
        hasTexCoords = true;
      }

      techniqueUniforms[uniformName] = {
        type: uniformType,
      };
    }
  }

  // Give the diffuse uniform a semantic to support color replacement in 3D Tiles
  if (defined(techniqueUniforms.u_diffuse)) {
    techniqueUniforms.u_diffuse.semantic = "_3DTILESDIFFUSE";
  }

  // Copy light parameters into technique parameters
  if (defined(lightParameters)) {
    for (const lightParamName in lightParameters) {
      if (lightParameters.hasOwnProperty(lightParamName)) {
        uniformName = `u_${lightParamName}`;
        techniqueUniforms[uniformName] = lightParameters[lightParamName];
      }
    }
  }

  // Add uniforms to shaders
  for (uniformName in techniqueUniforms) {
    if (techniqueUniforms.hasOwnProperty(uniformName)) {
      const uniform = techniqueUniforms[uniformName];
      const arraySize = defined(uniform.count) ? `[${uniform.count}]` : "";
      if (
        (uniform.type !== WebGLConstants.FLOAT_MAT3 &&
          uniform.type !== WebGLConstants.FLOAT_MAT4) ||
        uniform.useInFragment
      ) {
        fragmentShader += `uniform ${webGLConstantToGlslType(
          uniform.type
        )} ${uniformName}${arraySize};\n`;
        delete uniform.useInFragment;
      } else {
        vertexShader += `uniform ${webGLConstantToGlslType(
          uniform.type
        )} ${uniformName}${arraySize};\n`;
      }
    }
  }

  // Add attributes with semantics
  let vertexShaderMain = "";
  if (hasSkinning) {
    vertexShaderMain +=
      "    mat4 skinMatrix =\n" +
      "        a_weight.x * u_jointMatrix[int(a_joint.x)] +\n" +
      "        a_weight.y * u_jointMatrix[int(a_joint.y)] +\n" +
      "        a_weight.z * u_jointMatrix[int(a_joint.z)] +\n" +
      "        a_weight.w * u_jointMatrix[int(a_joint.w)];\n";
  }

  // Add position always
  const techniqueAttributes = {
    a_position: {
      semantic: "POSITION",
    },
  };
  vertexShader += "attribute vec3 a_position;\n";
  vertexShader += "varying vec3 v_positionEC;\n";
  if (hasSkinning) {
    vertexShaderMain +=
      "  vec4 pos = u_modelViewMatrix * skinMatrix * vec4(a_position,1.0);\n";
  } else {
    vertexShaderMain +=
      "  vec4 pos = u_modelViewMatrix * vec4(a_position,1.0);\n";
  }
  vertexShaderMain += "  v_positionEC = pos.xyz;\n";
  vertexShaderMain += "  gl_Position = u_projectionMatrix * pos;\n";
  fragmentShader += "varying vec3 v_positionEC;\n";

  // Add normal if we don't have constant lighting
  if (hasNormals) {
    techniqueAttributes.a_normal = {
      semantic: "NORMAL",
    };
    vertexShader += "attribute vec3 a_normal;\n";
    vertexShader += "varying vec3 v_normal;\n";
    if (hasSkinning) {
      vertexShaderMain +=
        "  v_normal = u_normalMatrix * mat3(skinMatrix) * a_normal;\n";
    } else {
      vertexShaderMain += "  v_normal = u_normalMatrix * a_normal;\n";
    }

    fragmentShader += "varying vec3 v_normal;\n";
  }

  // Add texture coordinates if the material uses them
  let v_texcoord;
  if (hasTexCoords) {
    techniqueAttributes.a_texcoord_0 = {
      semantic: "TEXCOORD_0",
    };

    v_texcoord = "v_texcoord_0";
    vertexShader += "attribute vec2 a_texcoord_0;\n";
    vertexShader += `varying vec2 ${v_texcoord};\n`;
    vertexShaderMain += `  ${v_texcoord} = a_texcoord_0;\n`;

    fragmentShader += `varying vec2 ${v_texcoord};\n`;
  }

  if (hasSkinning) {
    techniqueAttributes.a_joint = {
      semantic: "JOINTS_0",
    };
    techniqueAttributes.a_weight = {
      semantic: "WEIGHTS_0",
    };

    vertexShader += "attribute vec4 a_joint;\n";
    vertexShader += "attribute vec4 a_weight;\n";
  }

  if (hasVertexColors) {
    techniqueAttributes.a_vertexColor = {
      semantic: "COLOR_0",
    };
    vertexShader += "attribute vec4 a_vertexColor;\n";
    vertexShader += "varying vec4 v_vertexColor;\n";
    vertexShaderMain += "  v_vertexColor = a_vertexColor;\n";
    fragmentShader += "varying vec4 v_vertexColor;\n";
  }

  if (addBatchIdToGeneratedShaders) {
    techniqueAttributes.a_batchId = {
      semantic: "_BATCHID",
    };
    vertexShader += "attribute float a_batchId;\n";
  }

  const hasSpecular =
    hasNormals &&
    (lightingModel === "BLINN" || lightingModel === "PHONG") &&
    defined(techniqueUniforms.u_specular) &&
    defined(techniqueUniforms.u_shininess) &&
    techniqueUniforms.u_shininess > 0.0;

  // Generate lighting code blocks
  let hasNonAmbientLights = false;
  let hasAmbientLights = false;
  let fragmentLightingBlock = "";
  for (const lightName in lights) {
    if (lights.hasOwnProperty(lightName)) {
      const light = lights[lightName];
      const lightType = light.type.toLowerCase();
      const lightBaseName = light.baseName;
      fragmentLightingBlock += "  {\n";
      const lightColorName = `u_${lightBaseName}Color`;
      if (lightType === "ambient") {
        hasAmbientLights = true;
        fragmentLightingBlock += `    ambientLight += ${lightColorName};\n`;
      } else if (hasNormals) {
        hasNonAmbientLights = true;
        const varyingDirectionName = `v_${lightBaseName}Direction`;
        const varyingPositionName = `v_${lightBaseName}Position`;

        if (lightType !== "point") {
          vertexShader += `varying vec3 ${varyingDirectionName};\n`;
          fragmentShader += `varying vec3 ${varyingDirectionName};\n`;

          vertexShaderMain += `  ${varyingDirectionName} = mat3(u_${lightBaseName}Transform) * vec3(0.,0.,1.);\n`;
          if (lightType === "directional") {
            fragmentLightingBlock += `    vec3 l = normalize(${varyingDirectionName});\n`;
          }
        }

        if (lightType !== "directional") {
          vertexShader += `varying vec3 ${varyingPositionName};\n`;
          fragmentShader += `varying vec3 ${varyingPositionName};\n`;

          vertexShaderMain += `  ${varyingPositionName} = u_${lightBaseName}Transform[3].xyz;\n`;
          fragmentLightingBlock += `    vec3 VP = ${varyingPositionName} - v_positionEC;\n`;
          fragmentLightingBlock += "    vec3 l = normalize(VP);\n";
          fragmentLightingBlock += "    float range = length(VP);\n";
          fragmentLightingBlock += `    float attenuation = 1.0 / (u_${lightBaseName}Attenuation.x + `;
          fragmentLightingBlock += `(u_${lightBaseName}Attenuation.y * range) + `;
          fragmentLightingBlock += `(u_${lightBaseName}Attenuation.z * range * range));\n`;
        } else {
          fragmentLightingBlock += "    float attenuation = 1.0;\n";
        }

        if (lightType === "spot") {
          fragmentLightingBlock += `    float spotDot = dot(l, normalize(${varyingDirectionName}));\n`;
          fragmentLightingBlock += `    if (spotDot < cos(u_${lightBaseName}FallOff.x * 0.5))\n`;
          fragmentLightingBlock += "    {\n";
          fragmentLightingBlock += "      attenuation = 0.0;\n";
          fragmentLightingBlock += "    }\n";
          fragmentLightingBlock += "    else\n";
          fragmentLightingBlock += "    {\n";
          fragmentLightingBlock += `        attenuation *= max(0.0, pow(spotDot, u_${lightBaseName}FallOff.y));\n`;
          fragmentLightingBlock += "    }\n";
        }

        fragmentLightingBlock += `    diffuseLight += ${lightColorName}* max(dot(normal,l), 0.) * attenuation;\n`;

        if (hasSpecular) {
          if (lightingModel === "BLINN") {
            fragmentLightingBlock += "    vec3 h = normalize(l + viewDir);\n";
            fragmentLightingBlock +=
              "    float specularIntensity = max(0., pow(max(dot(normal, h), 0.), u_shininess)) * attenuation;\n";
          } else {
            // PHONG
            fragmentLightingBlock +=
              "    vec3 reflectDir = reflect(-l, normal);\n";
            fragmentLightingBlock +=
              "    float specularIntensity = max(0., pow(max(dot(reflectDir, viewDir), 0.), u_shininess)) * attenuation;\n";
          }
          fragmentLightingBlock += `    specularLight += ${lightColorName} * specularIntensity;\n`;
        }
      }
      fragmentLightingBlock += "  }\n";
    }
  }

  if (!hasAmbientLights) {
    // Add an ambient light if we don't have one
    fragmentLightingBlock += "  ambientLight += vec3(0.2, 0.2, 0.2);\n";
  }

  if (!hasNonAmbientLights && lightingModel !== "CONSTANT") {
    fragmentShader += "#ifdef USE_CUSTOM_LIGHT_COLOR \n";
    fragmentShader += "uniform vec3 gltf_lightColor; \n";
    fragmentShader += "#endif \n";

    fragmentLightingBlock += "#ifndef USE_CUSTOM_LIGHT_COLOR \n";
    fragmentLightingBlock += "    vec3 lightColor = czm_lightColor;\n";
    fragmentLightingBlock += "#else \n";
    fragmentLightingBlock += "    vec3 lightColor = gltf_lightColor;\n";
    fragmentLightingBlock += "#endif \n";

    fragmentLightingBlock += "  vec3 l = normalize(czm_lightDirectionEC);\n";
    const minimumLighting = "0.2"; // Use strings instead of values as 0.0 -> 0 when stringified
    fragmentLightingBlock += `  diffuseLight += lightColor * max(dot(normal,l), ${minimumLighting});\n`;

    if (hasSpecular) {
      if (lightingModel === "BLINN") {
        fragmentLightingBlock += "  vec3 h = normalize(l + viewDir);\n";
        fragmentLightingBlock +=
          "  float specularIntensity = max(0., pow(max(dot(normal, h), 0.), u_shininess));\n";
      } else {
        // PHONG
        fragmentLightingBlock += "  vec3 reflectDir = reflect(-l, normal);\n";
        fragmentLightingBlock +=
          "  float specularIntensity = max(0., pow(max(dot(reflectDir, viewDir), 0.), u_shininess));\n";
      }

      fragmentLightingBlock +=
        "  specularLight += lightColor * specularIntensity;\n";
    }
  }

  vertexShader += "void main(void) {\n";
  vertexShader += vertexShaderMain;
  vertexShader += "}\n";

  fragmentShader += "void main(void) {\n";
  let colorCreationBlock = "  vec3 color = vec3(0.0, 0.0, 0.0);\n";
  if (hasNormals) {
    fragmentShader += "  vec3 normal = normalize(v_normal);\n";
    if (khrMaterialsCommon.doubleSided) {
      fragmentShader += "  if (czm_backFacing())\n";
      fragmentShader += "  {\n";
      fragmentShader += "    normal = -normal;\n";
      fragmentShader += "  }\n";
    }
  }

  let finalColorComputation;
  if (lightingModel !== "CONSTANT") {
    if (defined(techniqueUniforms.u_diffuse)) {
      if (techniqueUniforms.u_diffuse.type === WebGLConstants.SAMPLER_2D) {
        fragmentShader += `  vec4 diffuse = texture2D(u_diffuse, ${v_texcoord});\n`;
      } else {
        fragmentShader += "  vec4 diffuse = u_diffuse;\n";
      }
      fragmentShader += "  vec3 diffuseLight = vec3(0.0, 0.0, 0.0);\n";
      colorCreationBlock += "  color += diffuse.rgb * diffuseLight;\n";
    }

    if (hasSpecular) {
      if (techniqueUniforms.u_specular.type === WebGLConstants.SAMPLER_2D) {
        fragmentShader += `  vec3 specular = texture2D(u_specular, ${v_texcoord}).rgb;\n`;
      } else {
        fragmentShader += "  vec3 specular = u_specular.rgb;\n";
      }
      fragmentShader += "  vec3 specularLight = vec3(0.0, 0.0, 0.0);\n";
      colorCreationBlock += "  color += specular * specularLight;\n";
    }

    if (defined(techniqueUniforms.u_transparency)) {
      finalColorComputation =
        "  gl_FragColor = vec4(color * diffuse.a * u_transparency, diffuse.a * u_transparency);\n";
    } else {
      finalColorComputation =
        "  gl_FragColor = vec4(color * diffuse.a, diffuse.a);\n";
    }
  } else if (defined(techniqueUniforms.u_transparency)) {
    finalColorComputation =
      "  gl_FragColor = vec4(color * u_transparency, u_transparency);\n";
  } else {
    finalColorComputation = "  gl_FragColor = vec4(color, 1.0);\n";
  }

  if (hasVertexColors) {
    colorCreationBlock += "  color *= v_vertexColor.rgb;\n";
  }

  if (defined(techniqueUniforms.u_emission)) {
    if (techniqueUniforms.u_emission.type === WebGLConstants.SAMPLER_2D) {
      fragmentShader += `  vec3 emission = texture2D(u_emission, ${v_texcoord}).rgb;\n`;
    } else {
      fragmentShader += "  vec3 emission = u_emission.rgb;\n";
    }
    colorCreationBlock += "  color += emission;\n";
  }

  if (defined(techniqueUniforms.u_ambient) || lightingModel !== "CONSTANT") {
    if (defined(techniqueUniforms.u_ambient)) {
      if (techniqueUniforms.u_ambient.type === WebGLConstants.SAMPLER_2D) {
        fragmentShader += `  vec3 ambient = texture2D(u_ambient, ${v_texcoord}).rgb;\n`;
      } else {
        fragmentShader += "  vec3 ambient = u_ambient.rgb;\n";
      }
    } else {
      fragmentShader += "  vec3 ambient = diffuse.rgb;\n";
    }
    colorCreationBlock += "  color += ambient * ambientLight;\n";
  }
  fragmentShader += "  vec3 viewDir = -normalize(v_positionEC);\n";
  fragmentShader += "  vec3 ambientLight = vec3(0.0, 0.0, 0.0);\n";

  // Add in light computations
  fragmentShader += fragmentLightingBlock;

  fragmentShader += colorCreationBlock;
  fragmentShader += finalColorComputation;
  fragmentShader += "}\n";

  // Add shaders
  const vertexShaderId = addToArray(shaders, {
    type: WebGLConstants.VERTEX_SHADER,
    extras: {
      _pipeline: {
        source: vertexShader,
        extension: ".glsl",
      },
    },
  });

  const fragmentShaderId = addToArray(shaders, {
    type: WebGLConstants.FRAGMENT_SHADER,
    extras: {
      _pipeline: {
        source: fragmentShader,
        extension: ".glsl",
      },
    },
  });

  // Add program
  const programId = addToArray(programs, {
    fragmentShader: fragmentShaderId,
    vertexShader: vertexShaderId,
  });

  const techniqueId = addToArray(techniques, {
    attributes: techniqueAttributes,
    program: programId,
    uniforms: techniqueUniforms,
  });

  return techniqueId;
}

function getKHRMaterialsCommonValueType(paramName, paramValue) {
  let value;

  // Backwards compatibility for COLLADA2GLTF v1.0-draft when it encoding
  // materials using KHR_materials_common with explicit type/value members
  if (defined(paramValue.value)) {
    value = paramValue.value;
  } else if (defined(paramValue.index)) {
    value = [paramValue.index];
  } else {
    value = paramValue;
  }

  switch (paramName) {
    case "ambient":
      return value.length === 1
        ? WebGLConstants.SAMPLER_2D
        : WebGLConstants.FLOAT_VEC4;
    case "diffuse":
      return value.length === 1
        ? WebGLConstants.SAMPLER_2D
        : WebGLConstants.FLOAT_VEC4;
    case "emission":
      return value.length === 1
        ? WebGLConstants.SAMPLER_2D
        : WebGLConstants.FLOAT_VEC4;
    case "specular":
      return value.length === 1
        ? WebGLConstants.SAMPLER_2D
        : WebGLConstants.FLOAT_VEC4;
    case "shininess":
      return WebGLConstants.FLOAT;
    case "transparency":
      return WebGLConstants.FLOAT;

    // these two are usually not used directly within shaders,
    // they are just added here for completeness
    case "transparent":
      return WebGLConstants.BOOL;
    case "doubleSided":
      return WebGLConstants.BOOL;
  }
}

function getTechniqueKey(khrMaterialsCommon, primitiveInfo) {
  let techniqueKey = "";
  techniqueKey += `technique:${khrMaterialsCommon.technique};`;

  const values = khrMaterialsCommon.values;
  const keys = Object.keys(values).sort();
  const keysCount = keys.length;
  for (let i = 0; i < keysCount; ++i) {
    const name = keys[i];
    if (values.hasOwnProperty(name)) {
      techniqueKey += `${name}:${getKHRMaterialsCommonValueType(
        name,
        values[name]
      )}`;
      techniqueKey += ";";
    }
  }

  const jointCount = defaultValue(khrMaterialsCommon.jointCount, 0);
  techniqueKey += `${jointCount.toString()};`;
  if (defined(primitiveInfo)) {
    const skinningInfo = primitiveInfo.skinning;
    if (jointCount > 0) {
      techniqueKey += `${skinningInfo.type};`;
    }
    techniqueKey += primitiveInfo.hasVertexColors;
  }

  return techniqueKey;
}

function lightDefaults(gltf) {
  const khrMaterialsCommon = gltf.extensions.KHR_materials_common;
  if (!defined(khrMaterialsCommon) || !defined(khrMaterialsCommon.lights)) {
    return;
  }

  const lights = khrMaterialsCommon.lights;

  const lightsLength = lights.length;
  for (let lightId = 0; lightId < lightsLength; lightId++) {
    const light = lights[lightId];
    if (light.type === "ambient") {
      if (!defined(light.ambient)) {
        light.ambient = {};
      }
      const ambientLight = light.ambient;

      if (!defined(ambientLight.color)) {
        ambientLight.color = [1.0, 1.0, 1.0];
      }
    } else if (light.type === "directional") {
      if (!defined(light.directional)) {
        light.directional = {};
      }
      const directionalLight = light.directional;

      if (!defined(directionalLight.color)) {
        directionalLight.color = [1.0, 1.0, 1.0];
      }
    } else if (light.type === "point") {
      if (!defined(light.point)) {
        light.point = {};
      }
      const pointLight = light.point;

      if (!defined(pointLight.color)) {
        pointLight.color = [1.0, 1.0, 1.0];
      }

      pointLight.constantAttenuation = defaultValue(
        pointLight.constantAttenuation,
        1.0
      );
      pointLight.linearAttenuation = defaultValue(
        pointLight.linearAttenuation,
        0.0
      );
      pointLight.quadraticAttenuation = defaultValue(
        pointLight.quadraticAttenuation,
        0.0
      );
    } else if (light.type === "spot") {
      if (!defined(light.spot)) {
        light.spot = {};
      }
      const spotLight = light.spot;

      if (!defined(spotLight.color)) {
        spotLight.color = [1.0, 1.0, 1.0];
      }

      spotLight.constantAttenuation = defaultValue(
        spotLight.constantAttenuation,
        1.0
      );
      spotLight.fallOffAngle = defaultValue(spotLight.fallOffAngle, 3.14159265);
      spotLight.fallOffExponent = defaultValue(spotLight.fallOffExponent, 0.0);
      spotLight.linearAttenuation = defaultValue(
        spotLight.linearAttenuation,
        0.0
      );
      spotLight.quadraticAttenuation = defaultValue(
        spotLight.quadraticAttenuation,
        0.0
      );
    }
  }
}
export default processModelMaterialsCommon;
